﻿namespace Okazuki.MVVM.PrismSupport.ViewModels.Utils
{
    using System;
    using System.Collections.Generic;
    using Microsoft.Practices.Prism.Commands;
    using System.Linq.Expressions;
    using System.Reflection;
    using System.Windows.Input;

    /// <summary>
    /// ViewModelの初期化処理を行うクラス
    /// </summary>
    internal static class ViewModelInitializer
    {
        /// <summary>
        /// ViweModelの型ごとの初期化処理のキャッシュ
        /// </summary>
        private static Dictionary<Type, Action<ViewModelBase>> InitializeProcessCache = new Dictionary<Type, Action<ViewModelBase>>();

        private static MethodInfo MakeEventHandlerMethodInfo = typeof(ViewModelInitializer).GetMethod("MakeEventHandler", BindingFlags.Static | BindingFlags.NonPublic);

        private static MethodInfo AddMethodInfo = typeof(ICollection<EventHandler>).GetMethod("Add", BindingFlags.Public | BindingFlags.Instance);

        private static MethodInfo AddRequerySuggestedMethodInfo = typeof(CommandManager).GetMethod("add_RequerySuggested");

        private static object SyncObject = new object();

        /// <summary>
        /// CreateDelegateのMethodInfo
        /// </summary>
        private static MethodInfo CreateDelegateMethodInfo = typeof(Delegate).GetMethod(
            "CreateDelegate",
            new[] { typeof(Type), typeof(object), typeof(MethodInfo) });

        /// <summary>
        /// ViewModelの初期化を行います。
        /// </summary>
        /// <param name="viewModel"></param>
        public static void InitializeViewModel(ViewModelBase viewModel)
        {
            if (viewModel == null)
            {
                throw new ArgumentNullException("viewModel");
            }

            var initializeProcess = GetInitializeProcess(viewModel.GetType());
            initializeProcess(viewModel);
        }

        /// <summary>
        /// ViewModelの型に対応した初期化処理を返す
        /// </summary>
        /// <param name="viewModelType"></param>
        /// <returns></returns>
        private static Action<ViewModelBase> GetInitializeProcess(Type viewModelType)
        {
            if (InitializeProcessCache.ContainsKey(viewModelType))
            {
                return InitializeProcessCache[viewModelType];
            }

            lock (SyncObject)
            {
                if (InitializeProcessCache.ContainsKey(viewModelType))
                {
                    return InitializeProcessCache[viewModelType];
                }

                var metadata = ViewModelReflectionUtil.GetViewModelInfo(viewModelType);
                return CreateAndRegistInitializeProcess(viewModelType, metadata);
            }
        }

        internal static Action<ViewModelBase> CreateAndRegistInitializeProcess(Type viewModelType, ViewModelTypeInfo metadata)
        {
            var parameter = Expression.Parameter(typeof(ViewModelBase));
            var cast = Expression.Convert(parameter, viewModelType);

            var expressions = new List<Expression>();// { cast };
            foreach (var commandProperty in metadata.AutoInitCommands)
            {
                expressions.Add(CreateInitCommandPropertyExpression(cast, commandProperty.Value));
            }

            var list = Expression.Property(
                parameter, "RequerySuggestedEventHandlers");
            foreach (var commandProperty in metadata.CommandProperties)
            {
                expressions.Add(CreateAddEventHandlerExpression(cast, list, commandProperty.Value));
            }

            if (expressions.Count == 0)
            {
                InitializeProcessCache.Add(viewModelType, EmptyInitAction);
                return EmptyInitAction;
            }

            var initProcesses = Expression.Block(expressions);

            var action = Expression.Lambda<Action<ViewModelBase>>(
                initProcesses,
                parameter).Compile();
            InitializeProcessCache.Add(viewModelType, action);
            return action;
        }

        private static void EmptyInitAction(ViewModelBase viewModel)
        {
        }

        private static Expression CreateInitCommandPropertyExpression(Expression viewModel, ViewModelCommandInfo commandProperty)
        {
            var executeMethodVar = Expression.Constant(
                commandProperty.ExecuteMethod);
            var canExecuteMethodVar = Expression.Constant(
                commandProperty.CanExecuteMethod);

            var executeMethodType = commandProperty.HasParameter ? Expression.GetActionType(commandProperty.ParameterType) : typeof(Action);
            var canExecuteMethodType = commandProperty.HasParameter ? Expression.GetFuncType(commandProperty.ParameterType, typeof(bool)) : typeof(Func<bool>);

            var delegateCommandType = commandProperty.HasParameter ?
                typeof(DelegateCommand<>).MakeGenericType(commandProperty.ParameterType) :
                typeof(DelegateCommand);
            var delegateCommandConstructorInfo = delegateCommandType.GetConstructor(
                commandProperty.HasCanExecuteMethod ? 
                    new Type[] { executeMethodType, canExecuteMethodType } :
                    new Type[] { executeMethodType });

            var callCreateExecuteMethodDelegate = Expression.Call(
                CreateDelegateMethodInfo,
                Expression.Constant(executeMethodType),
                viewModel,
                executeMethodVar);

            var callCreateCanExecuteMethodDelegate = commandProperty.HasCanExecuteMethod ?
                Expression.Call(
                    CreateDelegateMethodInfo,
                    Expression.Constant(canExecuteMethodType),
                    viewModel,
                    canExecuteMethodVar) :
                null;

            var newDelegateCommandExpression = 
                commandProperty.HasCanExecuteMethod ?
                    Expression.New(
                        delegateCommandConstructorInfo,
                        Expression.Convert(callCreateExecuteMethodDelegate, executeMethodType),
                        Expression.Convert(callCreateCanExecuteMethodDelegate, canExecuteMethodType)) :
                    Expression.New(
                        delegateCommandConstructorInfo,
                        Expression.Convert(callCreateExecuteMethodDelegate, executeMethodType));

            return Expression.Assign(
                Expression.Property(viewModel, commandProperty.CommandProperty), 
                newDelegateCommandExpression);
        }

        private static Expression CreateAddEventHandlerExpression(Expression viewModel, Expression list, PropertyInfo commandProperty)
        {
            // 組み立てたいツリーのイメージ
            /*
             * var command = viewModel.HogehogeCommand;
             * var eventHandler = MakeEventHandler(command);
             * CommandManager.RequerySuggested += eventHandler;
             * viewModel.RequerySuggestedEventHandlers.Add(eventHandler);
             */
            var eventHandlerVar = Expression.Variable(typeof(EventHandler), "evt");
            var assin = Expression.Assign(eventHandlerVar,
                Expression.Call(
                    MakeEventHandlerMethodInfo,
                    Expression.Property(viewModel, commandProperty)));
            var add = Expression.Call(list, AddMethodInfo, eventHandlerVar);
            var addEvent = Expression.Call(
                AddRequerySuggestedMethodInfo,
                eventHandlerVar);
            return Expression.Block(
                new[] { eventHandlerVar },
                assin,
                add,
                addEvent
                );
        }


        /// <summary>
        /// CommandのCanExecuteを呼び出すデリゲートを作成する
        /// </summary>
        /// <param name="command"></param>
        /// <returns></returns>
        internal static EventHandler MakeEventHandler(DelegateCommandBase command)
        {
            return (sender, e) => command.RaiseCanExecuteChanged();
        }

    }
}
